package com.fox.imagedb.ui;

/*
 * @(#)MetalTreeUI.java	1.25 07/01/08
 *
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;

import javax.swing.JComponent;
import javax.swing.UIManager;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.plaf.metal.MetalTreeUI;
import javax.swing.text.StyledDocument;
import javax.swing.tree.AbstractLayoutCache;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import com.fox.imagedb.listener.PhylTreeSelectionListener;

/**
 * The metal look and feel implementation of <code>TreeUI</code>.
 * <p>
 * <code>MetalTreeUI</code> allows for configuring how to visually render the
 * spacing and delineation between nodes. The following hints are supported:
 * 
 * <table summary="Descriptions of supported hints: Angled, Horizontal, and None">
 * <tr>
 * <th>
 * <p align="left">
 * Angled
 * </p>
 * </th>
 * <td>A line is drawn connecting the child to the parent. For handling of the
 * root node refer to {@link javax.swing.JTree#setRootVisible} and
 * {@link javax.swing.JTree#setShowsRootHandles}.</td>
 * </tr>
 * <tr>
 * <th>
 * <p align="left">
 * Horizontal
 * </p>
 * </th>
 * <td>A horizontal line is drawn dividing the children of the root node.</td>
 * </tr>
 * <tr>
 * <th>
 * <p align="left">
 * None
 * </p>
 * </th>
 * <td>Do not draw any visual indication between nodes.</td>
 * </tr>
 * </table>
 * 
 * <p>
 * As it is typically impratical to obtain the <code>TreeUI</code> from the
 * <code>JTree</code> and cast to an instance of <code>MetalTreeUI</code> you
 * enable this property via the client property <code>JTree.lineStyle</code>.
 * For example, to switch to <code>Horizontal</code> style you would do:
 * <code>tree.putClientProperty("JTree.lineStyle", "Horizontal");</code>
 * <p>
 * The default is <code>Angled</code>.
 * 
 * @version 1.25 01/08/07
 * @author Tom Santos
 * @author Steve Wilson (value add stuff)
 */
public class PhylTreeUI extends BasicTreeUI {

	public static final int HORIZ_LINE_SIZE = 20;

	public static final int VERT_LINE_SIZE = 20;

	private static Color lineColor;

	private static final String LINE_STYLE = "JTree.lineStyle";

	private static final String LEG_LINE_STYLE_STRING = "Angled";

	private static final String HORIZ_STYLE_STRING = "Horizontal";

	private static final String NO_STYLE_STRING = "None";

	private static final int LEG_LINE_STYLE = 2;

	private static final int HORIZ_LINE_STYLE = 1;

	private static final int NO_LINE_STYLE = 0;

	private int lineStyle = LEG_LINE_STYLE;

	private PropertyChangeListener lineStyleListener = new LineListener();

	private StyledDocument propertiesStyledDocument;

	private PhylTreeLayoutCache phylTreeLayoutCache;

	// Boilerplate
	public static ComponentUI createUI(JComponent x) {
		return new MetalTreeUI();
	}

	public PhylTreeUI(StyledDocument styledDocument) {
		super();
		propertiesStyledDocument = styledDocument;
		phylTreeLayoutCache = new PhylTreeLayoutCache();
	}

	@Override
	public void installUI(JComponent c) {
		super.installUI(c);
		lineColor = UIManager.getColor("Tree.line");

		Object lineStyleFlag = c.getClientProperty(LINE_STYLE);
		decodeLineStyle(lineStyleFlag);
		c.addPropertyChangeListener(lineStyleListener);

	}

	@Override
	public void uninstallUI(JComponent c) {
		c.removePropertyChangeListener(lineStyleListener);
		super.uninstallUI(c);
	}

	/**
	 * this function converts between the string passed into the client property
	 * and the internal representation (currently and int)
	 * 
	 */
	protected void decodeLineStyle(Object lineStyleFlag) {
		if ((lineStyleFlag == null)
				|| lineStyleFlag.equals(LEG_LINE_STYLE_STRING)) {
			lineStyle = LEG_LINE_STYLE; // default case
		} else {
			if (lineStyleFlag.equals(NO_STYLE_STRING)) {
				lineStyle = NO_LINE_STYLE;
			} else if (lineStyleFlag.equals(HORIZ_STYLE_STRING)) {
				lineStyle = HORIZ_LINE_STYLE;
			}
		}

	}

	@Override
	protected int getHorizontalLegBuffer() {
		return 3;
	}

	/**
	 * Creates the object responsible for managing what is expanded, as well as
	 * the size of nodes.
	 */
	@Override
	protected AbstractLayoutCache createLayoutCache() {
		return phylTreeLayoutCache;
	}

	protected boolean isLocationInExpandControl(int row, int rowLevel,
			int mouseX, int mouseY) {
		if ((tree != null) && !isLeaf(row)) {
			int boxWidth;

			if (getExpandedIcon() != null) {
				boxWidth = getExpandedIcon().getIconWidth() + 6;
			} else {
				boxWidth = 8;
			}

			Insets i = tree.getInsets();
			int boxLeftX = (i != null) ? i.left : 0;

			boxLeftX += (((rowLevel + depthOffset - 1) * totalChildIndent) + getLeftChildIndent())
					- boxWidth / 2;

			int boxRightX = boxLeftX + boxWidth;

			return (mouseX >= boxLeftX) && (mouseX <= boxRightX);
		}
		return false;
	}

	@Override
	protected TreeSelectionListener createTreeSelectionListener() {
		return new PhylTreeSelectionListener(propertiesStyledDocument, tree,
				phylTreeLayoutCache);
	}

	@Override
	@SuppressWarnings("unchecked")
	public void paint(Graphics g, JComponent c) {
		if (tree != c) {
			throw new InternalError("incorrect component");
		}

		// Should never happen if installed for a UI
		if (treeState == null) {
			return;
		}

		Rectangle paintBounds = g.getClipBounds();
		Insets insets = tree.getInsets();
		TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y);
		Enumeration paintingEnumerator = treeState
				.getVisiblePathsFrom(initialPath);
		int row = treeState.getRowForPath(initialPath);
		int endY = paintBounds.y + paintBounds.height;

		drawingCache.clear();

		if ((initialPath != null) && (paintingEnumerator != null)) {
			TreePath parentPath = initialPath;

			// Draw the lines, knobs, and rows

			boolean done = false;
			// Information for the node being rendered.
			boolean isLeaf;
			Rectangle boundsBuffer = new Rectangle();
			Rectangle bounds;
			TreePath path;
			boolean rootVisible = isRootVisible();

			while (!done && paintingEnumerator.hasMoreElements()) {
				path = (TreePath) paintingEnumerator.nextElement();
				if (path != null) {
					isLeaf = treeModel.isLeaf(path.getLastPathComponent());

					bounds = getPathBounds(path, insets, boundsBuffer);
					if (bounds == null) {
						// This will only happen if the model changes out
						// from under us (usually in another thread).
						// Swing isn't multithreaded, but I'll put this
						// check in anyway.
						return;
					}
					// See if the vertical line to the parent has been drawn.
					parentPath = path.getParentPath();
					paintRow(g, paintBounds, insets, bounds, path, row, isLeaf);
					if (parentPath != null) {
						if (drawingCache.get(parentPath) == null) {

							paintVerticalPartOfLeg(g, paintBounds, insets,
									parentPath);
							drawingCache.put(parentPath, Boolean.TRUE);
						}
						paintHorizontalPartOfLeg(g, paintBounds, bounds, path);
					} else if (rootVisible && (row == 0)) {
						paintHorizontalPartOfLeg(g, paintBounds, bounds, path);
					}

					if ((bounds.y + bounds.height) >= endY && isLeaf) {
						done = true;
					}
				} else {
					done = true;
				}
				row++;
			}
		}

		// Empty out the renderer pane, allowing renderers to be gc'ed.
		rendererPane.removeAll();

		// Paint the lines
		if ((lineStyle == HORIZ_LINE_STYLE) && !largeModel) {
			// paintHorizontalSeparators(g, c);
		}
	}

	/**
	 * Paints the renderer part of a row. The receiver should NOT modify
	 * <code>clipBounds</code>, or <code>insets</code>.
	 */
	protected void paintRow(Graphics g, Rectangle clipBounds, Insets insets,
			Rectangle bounds, TreePath path, int row, boolean isLeaf) {
		super.paintRow(g, clipBounds, insets, bounds, path, row, true, true,
				isLeaf);
	}

	protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds,
			Rectangle bounds, TreePath path) {
		// Don't paint the legs for the root'ish node if the
		int depth = path.getPathCount() - 1;
		if (((depth == 0) || ((depth == 1) && !isRootVisible()))
				&& !getShowsRootHandles()) {
			return;
		}
		int clipLeft = clipBounds.x;
		int clipRight = clipBounds.x + clipBounds.width;
		int clipTop = clipBounds.y;
		int clipBottom = clipBounds.y + clipBounds.height;
		int lineY = bounds.y + bounds.height / 2;

		int leftX = getPathLineIndent(path) + 5;

		int nodeX = 0;
		if (path.getLastPathComponent().toString().equals("")) {
			nodeX = bounds.x + bounds.width + 5;
		} else {
			nodeX = bounds.x - getHorizontalLegBuffer();
		}

		if ((lineY >= clipTop) && (lineY < clipBottom) && (nodeX >= clipLeft)
				&& (leftX < clipRight) && (leftX < nodeX)) {

			g.setColor(getHashColor());
			paintHorizontalLine(g, tree, lineY, leftX, nodeX - 1);
		}
		// if (!treeModel.isLeaf(path.getLastPathComponent())) {
		// int rightX = bounds.x + bounds.width;
		// paintHorizontalLine(g, tree, lineY, rightX, rightX
		// + HORIZ_LINE_SIZE);
		// }
	}

	@Override
	protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds,
			Insets insets, TreePath path) {

		int depth = path.getPathCount() - 1;
		if ((depth == 0) && !getShowsRootHandles() && !isRootVisible()) {
			return;
		}

		Rectangle parentBounds = getPathBounds(tree, path);
		int lineX = parentBounds.x + parentBounds.width + 5;

		int clipLeft = clipBounds.x;
		int clipRight = clipBounds.x + (clipBounds.width - 1);

		if ((lineX >= clipLeft) && (lineX <= clipRight)) {

			int clipTop = clipBounds.y;
			int clipBottom = clipBounds.y + clipBounds.height;

			Rectangle lastChildBounds = getPathBounds(tree,
					getLastChildPath(path));

			if (lastChildBounds == null) {
				// This shouldn't happen, but if the model is modified
				// in another thread it is possible for this to happen.
				// Swing isn't multithreaded, but I'll add this check in
				// anyway.
				return;
			}

			int top;

			if (parentBounds == null) {
				top = Math.max(insets.top + getVerticalLegBuffer(), clipTop);
			} else {
				top = Math.max(parentBounds.y + parentBounds.height / 2
						+ getVerticalLegBuffer(), clipTop);

			}
			if ((depth == 0) && !isRootVisible()) {
				TreeModel model = getModel();

				if (model != null) {
					Object root = model.getRoot();

					if (model.getChildCount(root) > 0) {
						parentBounds = getPathBounds(tree,
								path.pathByAddingChild(model.getChild(root, 0)));
						if (parentBounds != null) {
							top = Math.max(insets.top + getVerticalLegBuffer(),
									parentBounds.y + parentBounds.height / 2);
						}
					}
				}
			}

			int bottom = Math.min(lastChildBounds.y
					+ (lastChildBounds.height / 2), clipBottom);

			if (top <= bottom) {
				g.setColor(getHashColor());
				paintVerticalLine(g, tree, lineX, top, bottom);
			}
		}
	}

	@Override
	public void setSelectionModel(TreeSelectionModel newLSM) {
		// TODO Auto-generated method stub
		super.setSelectionModel(newLSM);
	}

	protected void paintHorizontalSeparators(Graphics g, JComponent c) {
		g.setColor(lineColor);

		Rectangle clipBounds = g.getClipBounds();

		int beginRow = getRowForPath(tree,
				getClosestPathForLocation(tree, 0, clipBounds.y));
		int endRow = getRowForPath(
				tree,
				getClosestPathForLocation(tree, 0, clipBounds.y
						+ clipBounds.height - 1));

		if ((beginRow <= -1) || (endRow <= -1)) {
			return;
		}

		for (int i = beginRow; i <= endRow; ++i) {
			TreePath path = getPathForRow(tree, i);

			if ((path != null) && (path.getPathCount() == 2)) {
				Rectangle rowBounds = getPathBounds(tree,
						getPathForRow(tree, i));

				// Draw a line at the top
				if (rowBounds != null) {
					g.drawLine(clipBounds.x, rowBounds.y, clipBounds.x
							+ clipBounds.width, rowBounds.y);
				}
			}
		}

	}

	private Rectangle getPathBounds(TreePath path, Insets insets,
			Rectangle bounds) {
		bounds = treeState.getBounds(path, bounds);
		if (bounds != null) {
			bounds.x += insets.left;
			bounds.y += insets.top;
		}
		return bounds;
	}

	private int getPathLineIndent(TreePath path) {
		return ((PhylTreeLayoutCache) treeState).getPathLineIndent(path);
	}

	/** This class listens for changes in line style */
	class LineListener implements PropertyChangeListener {

		public void propertyChange(PropertyChangeEvent e) {
			String name = e.getPropertyName();
			if (name.equals(LINE_STYLE)) {
				decodeLineStyle(e.getNewValue());
			}
		}
	} // end class PaletteListener

	/**
	 * Creates an instance of NodeDimensions that is able to determine the size
	 * of a given node in the tree.
	 */
	@Override
	protected AbstractLayoutCache.NodeDimensions createNodeDimensions() {
		return new TestNodeDimensionsHandler();
	}

	public class TestNodeDimensionsHandler extends
			BasicTreeUI.NodeDimensionsHandler {

		// End of class BasicTreeUI.NodeDimensionsHandler

		/**
		 * Responsible for getting the size of a particular node.
		 */
		@Override
		public Rectangle getNodeDimensions(Object value, int row, int depth,
				boolean expanded, Rectangle size) {
			// Return size of editing component, if editing and asking
			// for editing row.
			if ((editingComponent != null) && (editingRow == row)) {
				Dimension prefSize = editingComponent.getPreferredSize();
				int rh = getRowHeight();

				if ((rh > 0) && (rh != prefSize.height)) {
					prefSize.height = rh;
				}
				if (size != null) {
					size.width = prefSize.width;
					size.height = prefSize.height;
				} else {
					size = new Rectangle(0, 0, prefSize.width, prefSize.height);
				}
				return size;
			}
			// Not editing, use renderer.
			if (currentCellRenderer != null) {
				Component aComponent;

				aComponent = currentCellRenderer.getTreeCellRendererComponent(
						tree, value, tree.isRowSelected(row), expanded,
						treeModel.isLeaf(value), row, false);

				if (tree != null) {
					// Only ever removed when UI changes, this is OK!
					rendererPane.add(aComponent);
					aComponent.validate();
				}
				Dimension prefSize = aComponent.getPreferredSize();

				if (size != null) {
					size.width = prefSize.width < 20 ? 50 : prefSize.width;
					size.height = prefSize.height;
				} else {
					size = new Rectangle(0, 0, prefSize.width, prefSize.height);
				}
				return size;
			}
			return null;
		}
	}
}
